
Security News
Deno 2.2 Improves Dependency Management and Expands Node.js Compatibility
Deno 2.2 enhances Node.js compatibility, improves dependency management, adds OpenTelemetry support, and expands linting and task automation for developers.
prompt-anything
Advanced tools
Framework to build a a tree of modular and interactable prompts for anything
A modular and customizable framework to build prompts of any kind (such as ones within the console)! Originally inspired by the need to create console-like prompts in other applications such as chatting with bots.
npm install prompt-anything
interface VisualInterface = {
text: string;
}
interface MessageInterface {
content: string;
}
interface ChannelInterface<MessageType extends MessageInterface> {
send: (visual: VisualInterface) => Promise<MessageType|MessageType[]>;
}
Prompt
class must be extended to implement the abstract methods:
createCollector
- Returns an event emitter that should also emit message
whenever your collector gets a messageonReject
- Handles Rejection
errors (see the rejecting input section)stop
.exit
event for the user to prematurely exitclass MyPrompt<DataType, MessageType> extends Prompt<DataType, MessageType> {
createCollector(channel: ChannelInterface<MessageType>, data: DataType): PromptCollector<DataType, MessageType> {
const emitter: PromptCollector<DataType, MessageType> = new EventEmitter()
// Collect your messages via your listeners, and return an emitter that follows these rules
myCollector.on('myMessage', (message: MessageType) => {
// Emit the messages from your collector here
emitter.emit('message', message)
// Optionally allow exits
if (message === 'exit') {
emitter.emit('exit')
}
})
emitter.once('stop', () => {
// Stop your collector here
myCollector.stop()
})
return emitter
}
// Implement abstract methods. These events are automatically called
abstract async onReject(error: Errors.Rejection, message: MessageType, channel: ChannelInterface<MessageType>): Promise<void>;
}
See the examples/console.ts
for a functioning implementation that accepts input from the console.
A prompt is composed of two parts:
VisualInterface|VisualGenerator
- A object or function that determines how the prompt looks like to the userPromptFunction
- An (ideally pure) function that runs on every input from your collector// Data type that is passed to each prompt
type MyData = {
human?: boolean;
name?: string;
age?: number;
}
const askNameVisual: VisualInterface = {
text: 'What is your name?'
}
// askNameFn is run on every message collected during this prompt. This should be a pure function. (see below for details)
const askNameFn: PromptFunction<MyData, MessageType> = async (m: MessageType, data: MyData) => {
// This data is returned to the next prompt
return {
...data,
name: m.content
}
}
// Third argument is the optional PromptCondition
const askNamePrompt = new MyPrompt<MyData, MessageType>(askNameVisual, askNameFn)
The PromptFunction
should be pure function to
As a result, the function should always be referencing the original data variable passed from the previous prompt, regardless of how many times the function is run.
If you want a prompt's visual to be dependent on the given data, you can pass a function as the argument of a Prompt
instead of an object.
const askNameVisual = async (data: MyData): Promise<VisualInterface> => ({
text: `Hello ${data.human ? 'non-human' : 'human'}! What is your name?`
})
const askNamePrompt = new MyPrompt<MyData, MessageType>(askNameVisual, askNameFn)
To reject input, you can check the the content of the message in PromptFunction
, and throw a Errors.Rejection
. Upon throwing it:
send
methodconst askAgeFn: PromptFunction<MyData, MessageType> = async (m: MessageType, data: MyData) => {
const age = Number(m.content)
if (isNaN(age)) {
throw new Errors.Rejection(`That's not a valid number! Try again.`)
}
return {
...data,
age
}
}
To skip message collecting and only send a prompt's visual (usually done at the end of prompts), simply leave the second argument of Prompt
as undefined
.
const askNameVisual = {
text: 'The end is nigh'
}
const askNamePrompt = new MyPrompt<MyData, MessageType>(askNameVisual)
To automatically end message collection after a set duration, pass your duration in milliseconds as the 3rd argument to Prompt
.
const duration = 90000
const askNamePrompt = new MyPrompt<MyData, MessageType>(askNameVisual, askNameFn, duration)
This causes a Errors.UserInactivityError
to be thrown when the timeout is reached. The default value is 90000.
To connect prompts, you must put them into nodes and connect nodes together by setting their children. This allows prompts to be reused by attaching children to nodes instead of prompts.
const askNameNode = new PromptNode<MyData, MessageType>(askNamePrompt)
const askAgeNode = new PromptNode<MyData, MessageType>(askAgePrompt)
const askLocationNode = new PromptNode<MyData, MessageType>(askLocationPrompt)
askNameNode.addChild(askAgeNode)
askAgeNode.addChild(askLocationNode)
If you only want a node to run if it matches a condition (given the data from the previous prompt node), you can specify a condition function PromptNodeCondition
as the second argument of a PromptNode
.
// After we ask for the location, we'd like to send a prompt in a different language based on their input
const englishAskNodeCondition: PromptNodeCondition<MyData> = async (data) => !!data.location && data.location === 'loc1'
const englishAskNode = new PromptNode<MyData, MessageType>(englishAskPrompt, englishAskNodeCondition)
const spanishAskNodeCondition: PromptNodeCondition<MyData> = async (data) => !!data.location && data.location === 'loc2'
const spanishAskNode = new PromptNode<MyData, MessageType>(spanishAskPrompt, spanishAskNodeCondition)
askNameNode.addChild(askAgeNode)
askAgeNode.addChild(askLocationNode)
// addChild can be daisy-chained
askLocationNode
.addChild(englishAskNode)
.addChild(spanishAskNode)
// setChildren also works
askLocationNode.setChildren([englishAskNode, spanishAskNode])
The order of the children matters. The first child that matches its condition based on the given data will run. In this example, if englishAskPrompt
's condition function returns true
, then spanishAskNode
will never run.
After your prompt nodes are created, create a PromptRunner
that is initialized with the data you'll be passing to the first prompt, then call its run method with the first prompt node.
// The initial data that is given to the first prompt is passed to the PromptRunner's constructor
const runner = new PromptRunner<MyData, MessageType>({})
// run resolves with the data returned from the last prompt
const channel: ChannelInterface = myImplementedChannel()
const lastPromptData: MyData = await runner.run(askNameNode, channel)
// askName -> askAge -> askLocation -> (englishAsk OR spanishAsk)
// lastPromptData is the data returned from either englishAsk or spanishAsk
You can also run an array of prompt nodes. The first node that either has no condition, or has a matching condition will be passd to the run
method.
const runner = new PromptRunner<MyData>({})
// runArray resolves with the data returned from the last prompt
const channel: ChannelInterface<MessageType> = myImplementedChannel()
const lastPromptData: MyData = await runner.runArray([
askSurnameNode,
askNameNode
], channel)
// (askSurname OR askName) -> askAge -> askLocation -> (englishAsk OR spanishAsk)
Any error that throws within prompts will cause the PromptRunner
's run
to reject. In addition to regular errors, it may throw
Errors.UserVoluntaryExitError
if you emit exit
in createCollector
Errors.UserInactivityError
if timeout occurs (90000 ms by default)Both are instances of Errors.UserError
.
try {
const lastPromptData: MyData = await runner.run(node, channel)
} catch (err) {
if (err instanceof Errors.UserVoluntaryExitError) {
// show an exit message
} else if (err instanceof Errors.UserInactivityError) {
// show an expired message
} else {
// All other errors
}
}
Unit testing is straightforward since the tree of responses is built up from individual prompts that can be exported for testing. The prompts can be further decomposed into their visual, functional and conditional parts for even more granular tests.
Integration testing can be asserted on the execution order of the phases. Unfortunately, a "flush promises" method must be used since we cannot normally await
the promises while we are waiting for messages from EventEmitter
, otherwise the promise would never resolve until the series of prompts has ended.
async function flushPromises(): Promise<void> {
return new Promise(setImmediate);
}
type MockMessage = {
content: string;
}
const createMockMessage = (content = ''): MockMessage => ({
content
})
it('runs correctly for age <= 20', () => {
type AgeData = {
name?: string;
age?: number;
}
// Set up spies and the global emitter we'll use
const emitter: PromptCollector<AgeData, MessageType> = new EventEmitter()
const spy = jest.spyOn(MyPrompt.prototype, 'createCollector')
.mockReturnValue(emitter)
// Ask name Prompt that collects messages
const askNameFn: PromptFunction<AgeData, MessageType> = async function (m, data) {
return {
...data,
name: m.content
}
}
const askName = new MyPrompt<AgeData>(() => ({
text: `What's your name?`
}), askNameFn)
// Ask age Prompt that collects messages
const askAgeFn: PromptFunction<AgeData, MessageType> = async function (m, data) {
if (isNaN(Number(m.content))) {
throw new Errors.Rejection()
}
return {
...data,
age: Number(m.content)
}
}
const askAge = new MyPrompt<AgeData>((data) => ({
text: `How old are you, ${data.name}?`
}), askAgeFn)
// Conditional Prompt with no collector (MyPrompt)
const tooOld = new MyPrompt<AgeData>((data) => ({
text: `Wow ${data.name}, you are pretty old at ${data.age} years old!`
}), undefined, async (data) => !!data.age && data.age > 20)
// Conditional Prompt with no collector (MyPrompt)
const tooYoung = new MyPrompt<AgeData>((data) => ({
text: `Wow ${data.name}, you are pretty young at ${data.age} years old!`
}), undefined, async (data) => !!data.age && data.age <= 20)
const askNameNode = new PromptNode(askName)
const askAgeNode = new PromptNode(askAge)
const tooYoungNode = new PromptNode(tooYoung)
const tooOldNode = new PromptNode(tooOld)
askNameNode.setChildren([askAgeNode])
// Nodes with more than 1 sibling must have conditions defined
askAgeNode.setChildren([tooOldNode, tooYoungNode])
const message = createMockMessage()
const name = 'George'
const age = '30'
const runner = new PromptRunner<AgeData>()
const promise = runner.run(askNameNode, message)
// Wait for all pending promise callbacks to be executed for the emitter to set up
await flushPromises()
// Accept the name
emitter.emit('message', createMockMessage(name))
await flushPromises()
// Assert askName ran first
expect(runner.indexOf(askName)).toEqual(0)
// Accept the age
emitter.emit('message', createMockMessage(age))
await flushPromises()
// Assert askAge ran second
expect(runner.indexOf(askAge)).toEqual(1)
await promise
// Assert tooOld ran third, and tooYoung never ran
expect(runner.indexesOf([tooOld, tooYoung]))
.toEqual([2, -1])
// Clean up
spy.mockRestore()
})
FAQs
Framework to build a a tree of modular and interactable prompts for anything
The npm package prompt-anything receives a total of 14 weekly downloads. As such, prompt-anything popularity was classified as not popular.
We found that prompt-anything demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Deno 2.2 enhances Node.js compatibility, improves dependency management, adds OpenTelemetry support, and expands linting and task automation for developers.
Security News
React's CRA deprecation announcement sparked community criticism over framework recommendations, leading to quick updates acknowledging build tools like Vite as valid alternatives.
Security News
Ransomware payment rates hit an all-time low in 2024 as law enforcement crackdowns, stronger defenses, and shifting policies make attacks riskier and less profitable.